import torch
from torch import nn


# 本文件减少如何构建模型
# step1:Module类是nn模块里提供的一个模型构造类，是所有神经网络模块的基类
# Model是一个抽象的概念，即可以表示神经网络中的某一层，也可以表示一个包含多个层的神经网络。
# 我们可以继承它来定义我们想要的模型，下面继承Module类构造多层感知机
# 这里定义的MLP类重载了Module类的__init__函数和forward函数
# 它们分别用于创建模型参数和定义前向计算。前向计算也即正向传播
# 类中无须定义反向传播函数。nn.Module将通过自动求梯度而自动生成反向传播所需的backward函数。
class MLP(nn.Module):
    # 声明带有模型参数的层，这里声明了两个全连接层
    def __init__(self, **kwargs):
        # 调用MLP父类Module的构造函数来进行必要的初始化。这样在构造实例时还可以通过键值对的形式
        # 指定其他函数参数，如“模型参数的访问、初始化和共享”一节将介绍的模型参数params
        # **kwargs表示参数列表最后所有成对的键值对组成的字典
        # 这样就可以传入模型需要的其他参数了
        super(MLP, self).__init__(**kwargs)
        # 隐藏层，nn.Linear是用于设置网络中的全连接层的
        # 输入与输出都是二维张量，一般形状为[batch_size, size]
        # (784, 256)每个样本有784个特征值和10个输出
        # 可选bias，默认为true
        self.hidden = nn.Linear(784, 256)
        self.act = nn.ReLU()  # 激活函数ReLU
        self.output = nn.Linear(256, 10)  # 输出层，表示256个输入和10个输出

    # 定义模型的前向计算，即如何根据输入x计算返回所需要的模型输出
    def forward(self, x):
        a = self.act(self.hidden(x))  # 表示将x输入隐藏层，并将结果代入激活函数
        return self.output(a)  # 表示模型输出a


# 实例化MLP类得到模型变量net
# 初始化net并传入输入数据X做一次前向计算
# 其中，net(X)会调用MLP继承自Module类的__call__函数
# 这个函数将调用MLP类定义的forward函数来完成前向计算
X = torch.rand(2, 784)
net = MLP()
print(net)
net(X)


# Module类是一个通用的部件。
# 事实上，PyTorch还实现了继承自Module的可以方便构建模型的类:
# 如Sequential、ModuleList和ModuleDict等等。
# 首先介绍Sequential类
# 模型的前向计算为简单串联各个层的计算时，Sequential类可以通过更加简单的方式定义模型。
# 这正是Sequential类的目的：它可以接收一个子模块的有序字典（OrderedDict）
# 或者一系列子模块作为参数来逐一添加Module的实例，
# 而模型的前向计算就是将这些实例按添加的顺序逐一计算。
# 下面我们实现一个与Sequential类有相同功能的MySequential类。
# 这或许可以帮助读者更加清晰地理解Sequential类的工作机制。
class MySequential(nn.Module):
    def __init__(self, *args):
        from collections import OrderedDict
        super(MySequential, self).__init__()
        if len(args) == 1 and isinstance(args[0], OrderedDict): # 如果传入的是一个OrderedDict
            for key, module in args[0].items():
                self.add_module(key, module)  # add_module方法会将module添加进self._modules(一个OrderedDict)
        else:  # 传入的是一些Module
            for idx, module in enumerate(args):  # enumerate是枚举方法，返回列表中的索引和对应的值
                self.add_module(str(idx), module)

    def forward(self, x):
        # self._modules返回一个OrderedDict，保证会按照成员添加时的顺序遍历成员
        for module in self._modules.values():
            x = module(x)
        return x


# 用MySequential类来实现前面描述的MLP类，并使用随机初始化的模型做一次前向计算。
net = MySequential(
        nn.Linear(784, 256),
        nn.ReLU(),
        nn.Linear(256, 10),
        )
print(net)
net(X)


# ModuleList接收一个子模块的列表作为输入，然后也可以类似List那样进行append和extend操作:
net = nn.ModuleList([nn.Linear(784, 256), nn.ReLU()])
net.append(nn.Linear(256, 10))  # 类似List的append操作
print(net[-1])  # 类似List的索引访问
print(net)
# net(torch.zeros(1, 784)) # 会报NotImplementedError


# 既然Sequential和ModuleList都可以进行列表化构造网络，那二者区别是什么呢。
# ModuleList仅仅是一个储存各种模块的列表，
# 这些模块之间没有联系也没有顺序（所以不用保证相邻层的输入输出维度匹配），
# 而且没有实现forward功能需要自己实现，所以上面执行net(torch.zeros(1, 784))会报NotImplementedError；
# 而Sequential内的模块需要按照顺序排列，要保证相邻层的输入输出大小相匹配，内部forward功能已经实现。
# ModuleList的出现只是让网络定义前向传播时更加灵活，见下面官网的例子。
class MyModule(nn.Module):
    def __init__(self):
        super(MyModule, self).__init__()
        self.linears = nn.ModuleList([nn.Linear(10, 10) for i in range(10)])

    def forward(self, x):
        # ModuleList can act as an iterable, or be indexed using ints
        for i, l in enumerate(self.linears):
            x = self.linears[i // 2](x) + l(x)
        return x


# ModuleList不同于一般的Python的list
# 加入到ModuleList里面的所有模块的参数会被自动添加到整个网络中，下面看一个例子对比一下。
class Module_ModuleList(nn.Module):
    def __init__(self):
        super(Module_ModuleList, self).__init__()
        self.linears = nn.ModuleList([nn.Linear(10, 10)])


class Module_List(nn.Module):
    def __init__(self):
        super(Module_List, self).__init__()
        self.linears = [nn.Linear(10, 10)]


net1 = Module_ModuleList()
net2 = Module_List()

print("net1:")
for p in net1.parameters():
    print(p.size())

print("net2:")
for p in net2.parameters():
    print(p.size())


# ModuleDict接收一个子模块的字典作为输入, 然后也可以类似字典那样进行添加访问操作:
net = nn.ModuleDict({
    'linear': nn.Linear(784, 256),
    'act': nn.ReLU(),
})
net['output'] = nn.Linear(256, 10) # 添加
print(net['linear']) # 访问
print(net.output)
print(net)
# net(torch.zeros(1, 784)) # 会报NotImplementedError

# 下面做一个比较复杂的模型
class FancyMLP(nn.Module):
    def __init__(self, **kwargs):
        super(FancyMLP, self).__init__(**kwargs)

        self.rand_weight = torch.rand((20, 20), requires_grad=False)  # 不可训练参数（常数参数）
        self.linear = nn.Linear(20, 20)

    def forward(self, x):
        x = self.linear(x)  # 产生线性输出
        # 使用创建的常数参数，以及nn.functional中的relu函数和mm函数
        x = nn.functional.relu(torch.mm(x, self.rand_weight.data) + 1)  # mm是矩阵乘法

        # 复用全连接层。等价于两个全连接层共享参数
        x = self.linear(x)
        # 控制流，这里我们需要调用item函数来返回标量进行比较
        while x.norm().item() > 1:  # x的范数大于1则取一半
            x /= 2
        if x.norm().item() < 0.8:
            x *= 10
        return x.sum()


X = torch.rand(2, 20)
net = FancyMLP()
print(net)
net(X)


# 因为FancyMLP和Sequential类都是Module类的子类，所以我们可以嵌套调用它们。
class NestMLP(nn.Module):
    def __init__(self, **kwargs):
        super(NestMLP, self).__init__(**kwargs)
        self.net = nn.Sequential(nn.Linear(40, 30), nn.ReLU())

    def forward(self, x):
        return self.net(x)


net = nn.Sequential(NestMLP(), nn.Linear(30, 20), FancyMLP())
X = torch.rand(2, 40)
print(net)
net(X)